<!--
 * @Author: hongbin
 * @Date: 2024-07-30 07:48:37
 * @LastEditors: hongbin
 * @LastEditTime: 2024-11-01 19:31:02
 * @Description: 
-->
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>three.js webgpu - skinning instancing</title>
        <meta charset="utf-8" />
        <meta
            name="viewport"
            content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
        <link
            type="text/css"
            rel="stylesheet"
            href="../../three.js/examples/main.css" />
    </head>
    <body>
        <div id="info">
            <a
                href="https://threejs.org"
                target="_blank"
                rel="noopener"
                >three.js</a
            >
            webgpu - skinning instancing
        </div>

        <script type="importmap">
            {
                "imports": {
                    "three": "./three.webgpu.js",
                    "three/tsl": "./three.webgpu.js",
                    "three/addons/": "../../three.js/examples/jsm/"
                }
            }
        </script>

        <script type="module">
            import * as THREE from "three";
            import {
                pass,
                mix,
                range,
                color,
                oscSine,
                timerLocal,
                texture,
                TextureNode,
                normalLocal,
                min,
                max,
                abs,
                uniform,
                floor,
                float,
                nodeObject,
                instanceIndex,
                buffer,
                varyingProperty,
                int,
                instancedBufferAttribute,
                instancedDynamicBufferAttribute,
                vec3,
                reference,
            } from "three/tsl";

            import { GUI } from "three/addons/libs/lil-gui.module.min.js";
            import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
            import { OrbitControls } from "three/addons/controls/OrbitControls.js";
            import { RectAreaLightHelper } from "three/addons/helpers/RectAreaLightHelper.js";
            import { RectAreaLightTexturesLib } from "three/addons/lights/RectAreaLightTexturesLib.js";
            import TWEEN from "three/addons/libs/tween.module.js";

            let camera, scene, renderer, controls;
            let postProcessing, gui;

            let clock, delta;

            let dir = 1;

            let mixer, skinningInstanceMatrices;
            let mixer2, skinningInstanceMatrices2;
            let mixer3, skinningInstanceMatrices3;

            const tt = uniform(0, "float");
            const ttI = uniform(0, "float");

            class SkinningInstanceMatrices {
                constructor(mesh, count = 2, delay = 5, useTween = false) {
                    this.mesh = mesh;
                    this.count = count;
                    this.delay = 0.016 * delay;
                    this.time = 0;
                    this.useTween = useTween;

                    // const params = {opacity:1};
                    //     new TWEEN.Tween(params)
                    //         .to({ opacity: 0 }, 1000)
                    //         .onUpdate(function () {
                    //             console.log(new Date().getSeconds(),params.opacity)

                    //         })
                    //         .start();
                }

                setStatus(status = 1) {
                    this.isFinished = !status;

                    // this.mesh.setOmitOneOpacity && this.mesh.setOmitOneOpacity(status);

                    const params = { opacity: 1 - status };
                    new TWEEN.Tween(params)
                        .to({ opacity: status }, 16 * 10)
                        .onUpdate(() => {
                            this.mesh.setOmitOneOpacity && this.mesh.setOmitOneOpacity(params.opacity);
                        })
                        .start();
                }

                start() {
                    this.setStatus(1);
                }

                isFinished = false;

                /* 动画播放完 隐藏其他实例 */
                finished() {
                    this.setStatus(0);
                }

                prevAction(delta) {
                    this.time += delta;
                    if (this.time > this.delay && !this.isFinished) {
                        for (let i = this.count; i >= 1; i--) {
                            this.syncOnes(i, i - 1);

                            // if (this.useTween) {
                            //     const params = { opacity: 1 };

                            //     new TWEEN.Tween(params)
                            //         .to({ opacity: 0 }, this.delay * 1000)
                            //         .onUpdate(() => {
                            //             this.mesh.setOpacity && this.mesh.setOpacity(i, params.opacity);
                            //         })
                            //         .start();
                            // }
                        }

                        // this.syncOnes(3, 2);
                        // this.syncOnes(2, 1);
                        // this.syncOnes(1);
                        this.time = 0;

                        // const params = {opacity:1};
                        // new TWEEN.Tween(params)
                        //     .to({ opacity: 0 }, this.delay * 1000)
                        //     .onUpdate(function () {
                        //         // console.log(params.opacity)
                        //         window.setInstanceOpacity && window.setInstanceOpacity(1,params.opacity)
                        //         window.setInstanceOpacity && window.setInstanceOpacity(2,params.opacity)
                        //     })
                        //     .start();
                    }
                }

                syncOnes(index, copyIndex = 0) {
                    const length = this.mesh.skeleton.bones.length;

                    const copyIndexLeft = 16 * copyIndex * length;

                    // const end = 16 * (index + 1) * length;
                    const left = 16 * index * length;

                    // for (let index = left, i = 0; index < end; index++, i++) {
                    //     this.mesh.skeleton.boneMatrices[index] = this.mesh.skeleton.boneMatrices[i + copyIndexLeft];
                    // }

                    //使用内置方法 避免遍历 TypedArray.copyWithin(target start end) https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/copyWithin
                    // this.mesh.skeleton.boneMatrices.copyWithin(copyIndexLeft, 0, length * 16);
                    this.mesh.skeleton.boneMatrices.copyWithin(left, copyIndexLeft, copyIndexLeft + length * 16);
                }

                syncIndex(index) {
                    requestAnimationFrame(() => {
                        this.syncIndex(index);
                    });
                    this.syncOnes(index);
                    // const { count } = this.mesh;
                    // const length = this.mesh.skeleton.bones.length;

                    // const end = 16 * (index + 1) * length;
                    // const left = 16 * index * length;

                    // for (let index = left, i = 0; index < end; index++, i++) {
                    //     this.mesh.skeleton.boneMatrices[index] = this.mesh.skeleton.boneMatrices[i];
                    // }
                }
            }

            init();

            window.offsetCount = 1040;

            function init() {
                gui = new GUI();
                window.stopAnimation = false;
                gui.add(window, "stopAnimation");
                THREE.RectAreaLightNode.setLTC(RectAreaLightTexturesLib.init());

                camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 40);
                // camera.position.set( 1, 2, 3 );
                camera.position.set(0, 0, 0);

                scene = new THREE.Scene();

                // scene.add(new THREE.AxesHelper(1));

                camera.lookAt(0, 1, 0);

                clock = new THREE.Clock();

                // lights
                {
                    // const centerLight = new THREE.PointLight(0xffffff, 2, 100);
                    // centerLight.position.y = 4.5;
                    // centerLight.position.z = 2
                    // centerLight.power = 400;
                    // scene.add(centerLight);

                    // const ambientLight = new THREE.AmbientLight(0x404040);
                    // scene.add(ambientLight);

                    // const cameraLight = new THREE.PointLight(0xffffff, 1, 100);
                    // cameraLight.power = 400;
                    // cameraLight.position.set(0, 2, 3);
                    // camera.add(cameraLight);
                    // scene.add(camera);
                    // scene.add(cameraLight);

                    const rectLight1 = new THREE.RectAreaLight(0xffffff, 10, 1, 1);
                    rectLight1.position.set(0, 2, 0);
                    rectLight1.lookAt(0, -1, 0);
                    scene.add(rectLight1);

                    // {
                    //     const rectLight1 = new THREE.RectAreaLight(0xffffff, 1, 10, 0.5);
                    //     rectLight1.position.set(0, 2.1, 0);
                    //     rectLight1.lookAt(0, -1, 0);
                    //     scene.add(rectLight1);
                    // }
                    // {
                    //     const rectLight1 = new THREE.RectAreaLight(0xffffff, 10, 10, 0.1);
                    //     rectLight1.position.set(0, 0, 2);
                    //     rectLight1.lookAt(0, 0, 0);
                    //     scene.add(rectLight1);
                    // }
                    // scene.add(new RectAreaLightHelper(rectLight1));
                }

                const thickness = 2;
                const geometry = new THREE.BoxGeometry(2, 0.1, thickness);
                // geometry.translate(0, 0, -thickness / 2);
                // geometry.rotateX(-Math.PI / 2);

                // const m = new THREE.MeshStandardNodeMaterial({
                //     color: 0x000000,
                //     roughness: 0,
                //     metalness: 0.,
                // });

                const floorMesh = new THREE.Mesh(
                    geometry,
                    new THREE.MeshStandardMaterial({
                        color: 0xffffff,
                        roughness: 0.1,
                        metalness: 0.,
                    })
                    // m
                );

                // m.colorNode = texture(map);
                // const oscNode = abs(oscSine(timerLocal(0.1)));
                // m.metalnessNode = oscNode;

                scene.add(floorMesh);


                // {
                //     const floorMesh = new THREE.Mesh(
                //         new THREE.BoxGeometry(0.1, 0.1, 0.1),
                //         new THREE.MeshStandardMaterial({
                //             color: 0xff0000,
                //             roughness: 0,
                //             metalness: 0,
                //         })
                //     );
                //     floorMesh.position.set(0, 1, 1);
                //     scene.add(floorMesh);
                // }

                // {
                //     const geometry = new THREE.BoxGeometry(100, 2.4, 2);

                //     const map = new THREE.TextureLoader().load("../../three.js/examples/textures/carbon/Carbon.png");
                //     const normalMap = new THREE.TextureLoader().load(
                //         "../../three.js/examples/textures/carbon/Carbon_Normal.png"
                //     );

                //     map.colorSpace = THREE.SRGBColorSpace;
                //     normalMap.wrapS = map.wrapS = THREE.RepeatWrapping;
                //     normalMap.wrapT = map.wrapT = THREE.RepeatWrapping;
                //     map.repeat.set(60, 1);
                //     normalMap.repeat.copy(map.repeat);

                //     const box = new THREE.Mesh(
                //         geometry,
                //         new THREE.MeshStandardMaterial({
                //             color: 0xffffff,
                //             // color: 0x666666,
                //             roughness: 0.3,
                //             metalness: 0.7,
                //             map,
                //             normalMap,
                //         })
                //     );
                //     box.position.z = -2;
                //     box.position.y = 1.2;
                //     scene.add(box);
                // }
                
                window.load = false;

                if (window.load) {
                    const loader = new GLTFLoader();

                    loader.load("../../three.js/examples/models/gltf/Michelle.glb", function (gltf) {
                        const danceGUI = gui.addFolder("跳舞");
                        const object = gltf.scene;

                        mixer = new THREE.AnimationMixer(object);

                        const action = mixer.clipAction(gltf.animations[0]);
                        action.play();

                        const instanceCount = 10;
                        const dummy = new THREE.Object3D();

                        object.traverse(child => {
                            if (child.isMesh) {
                                skinningInstanceMatrices = new SkinningInstanceMatrices(child,0);

                                const oscNode = abs(oscSine(timerLocal(0.1)));
                                // const oscNode = abs(oscSine(timerLocal(1)));
                                // const oscNode = abs(timerLocal(1).sin());
                                const indexNode = floor(oscNode.div(0.3333334));
                                // const oscNode = oscSine(timerLocal(0.1));

                                const randomColors = range(new THREE.Color(0x000000), new THREE.Color(0xffffff));

                                const randomMetalness = range(0, 1);
                                const prevMap = child.material.map;

                                // 设置 false 不计入 视椎体可见范围计算 避免被隐藏  椎体计算 一般使用object.boundingSphere
                                child.frustumCulled = false;

                                // 显示卡通边框
                                child.CartoonBorder = true;
                                child.cartoonBorder = uniform(0.2);
                                child.cartoonBorderColor = uniform(new THREE.Color("#ffffff"));
                                child.CartoonBorderLight = uniform(vec3(0, 0, 1), "vec3");
                                // child.CartoonBorderLight = uniform(camera.position,'vec3');

                                danceGUI.add(child.cartoonBorder, "value", 0, 1).name("cartoonBorder");
                                danceGUI.add(skinningInstanceMatrices, "delay", 0, 0.2).name("speed");

                                danceGUI
                                    .addColor({ color: "#" + child.cartoonBorderColor.value.getHexString() }, "color")
                                    .onChange(c => {
                                        const newColor = new THREE.Color(c);
                                        child.cartoonBorderColor.value.copy(newColor);
                                    });

                                child.material = new THREE.MeshStandardNodeMaterial({
                                    transparent: true,
                                    side: 0,
                                    roughness:0,
                                    metalness:1
                                });

                                // child.material.onBeforeCompile = (shader) => {
                                // 	console.log("onBeforeCompile:", shader);
                                // };

                                // roughnessNode是变化的 roughness是固定的
                                // child.material.roughnessNode = oscNode;

                                // child.material.metalnessNode = 0.5 || mix(0.0, randomMetalness, oscNode);

                                // child.material.colorNode = mix(texture(prevMap), randomColors, oscNode);

                                child.material.colorNode = texture(prevMap);

                                child.isInstancedMesh = true;

                                child.instanceMatrix = new THREE.InstancedBufferAttribute(
                                    new Float32Array(instanceCount * 16),
                                    16
                                );

                                // 透明度
                                const floatOpacity = new Float32Array(instanceCount);

                                const buffer = new THREE.InstancedBufferAttribute(floatOpacity, 1);
                                console.log(buffer);
                                // instancedDynamicBufferAttribute 每次写入buffer
                                // instancedBufferAttribute 手动更新 needsUpdate = true
                                child.instanceOpacity = instancedBufferAttribute(buffer);

                                window.setInstanceOpacity = (index, opacity) => {
                                    floatOpacity[index] = opacity;
                                    buffer.needsUpdate = true;
                                };

                                // child.instanceOpacity = new THREE.InstancedBufferAttribute(
                                //     new Float32Array(instanceCount),
                                //     1
                                // );
                                // child.instanceOpacity.setUsage(THREE.DynamicDrawUsage);

                                // 索引 attribute
                                const floatIndex = new Float32Array(instanceCount);

                                const indexBuffer = new THREE.InstancedBufferAttribute(floatIndex, 1);
                                child.instanceIndex = instancedBufferAttribute(indexBuffer);

                                // 索引
                                // child.instanceIndex = new THREE.InstancedBufferAttribute(
                                //     new Float32Array(instanceCount),
                                //     1
                                // );

                                // 提供uniform
                                // 选中的实例索引
                                // child.selectInstanceIndex = ttI;
                                // child.selectInstanceIndex = indexNode;
                                child.selectInstanceIndex = uniform(-1, "float");

                                // 选中的实例索引的透明度
                                child.selectInstanceIndexOpacity = tt;
                                // child.selectInstanceIndexOpacity = uniform(0.5, "float");

                                // child.selectInstanceIndexOpacity = abs(oscSine(timerLocal(0.33334)));
                                // child.selectInstanceIndexOpacity = oscNode
                                child.count = instanceCount;
                                //重新设置 实例矩阵长度 为原长度的instanceCount倍
                                child.skeleton.setInstanceCount(instanceCount);

                                // gui.add(
                                //     {
                                //         f: () => {
                                //             mixer.update(delta);
                                //             console.log(child.skeleton.boneMatrices);
                                //         },
                                //     },
                                //     "f"
                                // ).name("mixer.update()");

                                for (let i = 0; i < instanceCount; i++) {
                                    // dummy.position.y = Math.floor(i / 5) * -200;
                                    dummy.position.x = 70;
                                    // dummy.position.x = i * 70;
                                    dummy.position.z = i * -0.1;

                                    dummy.updateMatrix();

                                    dummy.matrix.toArray(child.instanceMatrix.array, i * 16);

                                    floatIndex[i] = i;

                                    floatOpacity[i] = 1;
                                }

                                const setIndexFadeOut = i => {
                                    // child.selectInstanceIndex.value = i;
                                    // dir = -1;
                                    // tt.value = 1;
                                };

                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(1) }, "f").name("1 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(2) }, "f").name("2 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(3) }, "f").name("3 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(4) }, "f").name("4 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(5) }, "f").name("5 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(6) }, "f").name("6 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(7) }, "f").name("7 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(8) }, "f").name("8 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncOnes(9) }, "f").name("9 ONES");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(1) }, "f").name("1 ASYNC");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(2) }, "f").name("2 ASYNC");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(3) }, "f").name("3 ASYNC");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(4) }, "f").name("4 ASYNC");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(5) }, "f").name("5 ASYNC");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(6) }, "f").name("6 ASYNC");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(7) }, "f").name("7 ASYNC");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(8) }, "f").name("8 ASYNC");
                                // gui.add({ f: () => skinningInstanceMatrices.syncIndex(9) }, "f").name("9 ASYNC");
                            }
                        });

                        scene.add(object);
                    });
                }
                // renderer

                renderer = new THREE.WebGPURenderer({ antialias: true });
                renderer.setPixelRatio(2);
                renderer.setSize(window.innerWidth, window.innerHeight);
                renderer.setAnimationLoop(animate);
                document.body.appendChild(renderer.domElement);

                controls = new OrbitControls(camera, renderer.domElement);

                controls.target.set(0, 1, 0);
                controls.object.position.set(0, 1, 4);

                // post processing

                const scenePass = pass(scene, camera);
                const scenePassColor = scenePass.getTextureNode();
                const scenePassDepth = scenePass.getLinearDepthNode().remapClamp(0.15, 0.3);

                const scenePassColorBlurred = scenePassColor.gaussianBlur();
                scenePassColorBlurred.directionNode = scenePassDepth;

                // postProcessing = new THREE.PostProcessing(renderer);
                // postProcessing.outputNode = scenePassColorBlurred;

                // events

                window.addEventListener("resize", onWindowResize);
            }

            function onWindowResize() {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();

                renderer.setSize(window.innerWidth, window.innerHeight);
            }

            function animate() {
                delta = clock.getDelta();

                if (skinningInstanceMatrices) {
                    TWEEN.update();
                }
                if (!window.stopAnimation) {
                    skinningInstanceMatrices && skinningInstanceMatrices.prevAction(delta);
                    if (mixer) mixer.update(delta);
                    skinningInstanceMatrices2 && skinningInstanceMatrices2.prevAction(delta);
                    if (mixer2) mixer2.update(delta);
                    skinningInstanceMatrices3 && skinningInstanceMatrices3.prevAction(delta);
                    if (mixer3) mixer3.update(delta);
                }

                // tt.value = tt.value + 0.01 * dir;

                // if (tt.value >= 1 || tt.value <= 0) {
                //     dir *= -1;
                // }

                // ttI.value = Math.floor(tt.value / 0.333334);

                // postProcessing.render();
                renderer.render(scene, camera);
            }
        </script>
    </body>
</html>
